﻿using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;

namespace Notoric.ViewModel.Elements.CWNotation
{
    /// <summary>
    /// Adapts a <see cref="Model.Elements.CWNotation.System "/> instance to a Windows Presentation Foundation view.
    /// </summary>
    public class System : PositionableElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="System"/> instance.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="MusicSectionInterior"/>
        /// </para>
        /// <para>
        /// The element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="adaptedElement">
        /// <para>
        /// Type: <see cref="Model.Elements.CWNotation.System "/>
        /// </para>
        /// <para>
        /// The adapted model element instance.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="adaptedElement"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="NullReferenceException">
        /// <paramref name="owner"/> is <see langword="null"/>.
        /// The values of the <see cref="ViewModelElement.UndoStack"/> and <see cref="ViewModelElement.RedoStack"/> properties
        /// are passed directly to the base class (<see cref="ViewModelElement"/>) constructor. Thus, there is not
        /// an occasion to react with a more appropriate exception.
        /// </exception>
        public System(MusicSectionInterior owner, Model.Elements.CWNotation.System adaptedElement)
            : base(adaptedElement, owner, owner.UndoStack, owner.RedoStack, false)
        {
            barlines = new ObservableCollection<Barline>();
            connectors = new ObservableCollection<Connector>();
            repetitionDots = new ObservableCollection<Point>();
            staves = new ObservableCollection<Staff>();
            staffConnectors = new ObservableCollection<StaffConnector>();

            foreach (Model.Elements.CWNotation.Staff staff in adaptedElement.Staves)
            {
                var vmStaff = new Staff(this, staff);
                vmStaff.PropertyChanged += new PropertyChangedEventHandler(vmStaff_PropertyChanged);
                staves.Add(vmStaff);
                repetitionDots.Add(new Point(vmStaff.Bottom - 1.5 * vmStaff.SpaceSize, vmStaff.Bottom - 2.5 * vmStaff.SpaceSize));
            }
            foreach (Model.Elements.CWNotation.Barline barline in adaptedElement.Barlines)
                barlines.Add(new Barline(this, barline));
            foreach (Model.Elements.CWNotation.StaffConnector connector in adaptedElement.StaffConnectors)
                staffConnectors.Add(StaffConnector.CreateAdapter(this, connector));
            foreach (Model.Elements.CWNotation.Connector connector in adaptedElement.Connectors)
                connectors.Add(Connector.CreateAdapter(connector, this));

            adaptedElement.Staves.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(Staves_CollectionChanged);
            adaptedElement.Barlines.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(Barlines_CollectionChanged);
            adaptedElement.StaffConnectors.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(StaffConnectors_CollectionChanged);
            adaptedElement.Connectors.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(Connectors_CollectionChanged);
        }

        #endregion

        #region ViewModelElement Members

        /// <inheritdoc/>
        public override void Dispose()
        {
            var adaptedElement = (Model.Elements.CWNotation.System)AdaptedElement;
            adaptedElement.Barlines.CollectionChanged -= Barlines_CollectionChanged;
            adaptedElement.Staves.CollectionChanged -= Staves_CollectionChanged;

            foreach (Staff staff in staves)
                staff.Dispose();
            foreach (Barline barline in barlines)
                barline.Dispose();

            base.Dispose();
        }

        /// <inheritdoc/>
        public override string ElementNameKey
        {
            get { return elementNameKey; }
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the <see cref="Model.Elements.CWNotation.System.Barlines"/> collection of the <see cref="ViewModelElement.AdaptedElement"/> has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Barlines_CollectionChanged(object sender, Model.Collections.NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case Model.Collections.NotifyCollectionChangedAction.Add:
                {
                    int index = e.NewStartingIndex;
                    foreach (Model.Elements.CWNotation.Barline barline in e.NewItems)
                        barlines.Insert(index++, new Barline(this, barline));
                    break;
                }
                case Model.Collections.NotifyCollectionChangedAction.Remove:
                    foreach (Model.Elements.CWNotation.Barline barline in e.OldItems)
                        barlines.Remove(barlines.First(b => b.AdaptedElement == barline));
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Move:
                    barlines.Move(e.OldStartingIndex, e.NewStartingIndex);
                    break;
            }
        }

        /// <summary>
        /// Occurs when the <see cref="Model.Elements.CWNotation.System.Connectors"/> collection of the <see cref="ViewModelElement.AdaptedElement"/> has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Connectors_CollectionChanged(object sender, Model.Collections.NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case Model.Collections.NotifyCollectionChangedAction.Add:
                    foreach (Model.Elements.CWNotation.Connector connector in e.NewItems)
                        connectors.Add(Connector.CreateAdapter(connector, this));
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Remove:
                    foreach (Model.Elements.CWNotation.Connector connector in e.OldItems)
                        connectors.Remove(connectors.First(c => c.AdaptedElement == connector));
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Replace:
                    foreach (Model.Elements.CWNotation.Connector connector in e.OldItems)
                        connectors.Remove(connectors.First(c => c.AdaptedElement == connector));
                    foreach (Model.Elements.CWNotation.Connector connector in e.NewItems)
                        connectors.Add(Connector.CreateAdapter(connector, this));
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Reset:
                    connectors.Clear();
                    break;
            }
        }

        /// <summary>
        /// Occurs when the value of a property of a staff has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void vmStaff_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Bottom")
            {
                var staff = (Staff)sender;
                var index = staves.IndexOf(staff);

                if (index > -1)
                    repetitionDots[index] = new Point(staff.Bottom - 1.5 * staff.SpaceSize, staff.Bottom - 2.5 * staff.SpaceSize);
            }
        }

        /// <summary>
        /// Occurs when the <see cref="Model.Elements.CWNotation.System.Staves"/> collection of the <see cref="ViewModelElement.AdaptedElement"/> has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        void Staves_CollectionChanged(object sender, Model.Collections.NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case Model.Collections.NotifyCollectionChangedAction.Add:
                {
                    int index = e.NewStartingIndex;
                    foreach (Model.Elements.CWNotation.Staff staff in e.NewItems)
                    {
                        var vmStaff = new Staff(this, staff);
                        vmStaff.PropertyChanged += new PropertyChangedEventHandler(vmStaff_PropertyChanged);
                        repetitionDots.Insert(index, new Point(vmStaff.Bottom - 1.5 * vmStaff.SpaceSize, vmStaff.Bottom - 2.5 * vmStaff.SpaceSize));
                        staves.Insert(index++, vmStaff);
                    }
                    break;
                }
                case Model.Collections.NotifyCollectionChangedAction.Remove:
                    foreach (Model.Elements.CWNotation.Staff staff in e.OldItems)
                    {
                        var vmStaff = staves.First(s => s.AdaptedElement == staff);
                        vmStaff.PropertyChanged -= vmStaff_PropertyChanged;
                        repetitionDots.RemoveAt(staves.IndexOf(vmStaff));
                        staves.Remove(vmStaff);
                    }
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Move:
                    staves.Move(e.OldStartingIndex, e.NewStartingIndex);
                    repetitionDots.Move(e.OldStartingIndex, e.NewStartingIndex);
                    break;
            }
        }

        /// <summary>
        /// Occurs when the <see cref="Model.Elements.CWNotation.System.StaffConnectors"/> collection of the <see cref="ViewModelElement.AdaptedElement"/> has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void StaffConnectors_CollectionChanged(object sender, Model.Collections.NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case Model.Collections.NotifyCollectionChangedAction.Add:
                {
                    int index = e.NewStartingIndex;
                    foreach (Model.Elements.CWNotation.StaffConnector staffConnector in e.NewItems)
                        staffConnectors.Insert(index++, StaffConnector.CreateAdapter(this, staffConnector));
                    break;
                }
                case Model.Collections.NotifyCollectionChangedAction.Remove:
                    foreach (Model.Elements.CWNotation.StaffConnector staffConnector in e.OldItems)
                        staffConnectors.Remove(staffConnectors.First(s => s.AdaptedElement == staffConnector));
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Move:
                    staffConnectors.Move(e.OldStartingIndex, e.NewStartingIndex);
                    break;
            }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the collection of barlines.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ObservableCollection{T}"/> of <see cref="Barline"/>
        /// </para>
        /// <para>
        /// The collection of barlines.
        /// </para>
        /// </value>
        public ObservableCollection<Barline> Barlines
        {
            get { return barlines; }
        }

        /// <summary>
        /// Gets the collection of connectors.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ObservableCollection{T}"/> of <see cref="Connector"/>
        /// </para>
        /// <para>
        /// The collection of connectors.
        /// </para>
        /// </value>
        public ObservableCollection<Connector> Connectors
        {
            get { return connectors; }
        }

        /// <summary>
        /// Gets the collection of y-coordinates of the repetition dots.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ObservableCollection{T}"/> of <see cref="Point"/>
        /// </para>
        /// <para>
        /// The collection of vertical positions of repetition dots.<br/>
        /// This collection is intended for use by repetition barlines visualization classes.
        /// It helps them by providing them precomputed and automatically updated positions 
        /// of the repetition dots for each staff of the system.<br/>
        /// The <see cref="Point.X"/> property holds the vertical position of the upper dot
        /// and <see cref="Point.Y"/> property holds the vertical position of the lower dot.
        /// </para>
        /// </value>
        public ObservableCollection<Point> RepetitionDots
        {
            get { return repetitionDots; }
        }

        /// <summary>
        /// Gets the collection of the staves.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ObservableCollection{T}"/> of <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The collection of staves.
        /// </para>
        /// </value>
        public ObservableCollection<Staff> Staves
        {
            get { return staves; }
        }

        /// <summary>
        /// Gets the collection of staff connectors.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ObservableCollection{T}"/> of <see cref="StaffConnector"/>
        /// </para>
        /// <para>
        /// The collection of staff connectors.
        /// </para>
        /// </value>
        public ObservableCollection<StaffConnector> StaffConnectors
        {
            get { return staffConnectors; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Barlines"/> property.
        /// </summary>
        private ObservableCollection<Barline> barlines;
        /// <summary>
        /// Holds the value of the <see cref="Connectors"/> property.
        /// </summary>
        private ObservableCollection<Connector> connectors;
        /// <summary>
        /// Holds a user-friendly name of this element.
        /// </summary>
        private const string elementNameKey = "ViewModel.ElementNames.System";
        /// <summary>
        /// Holds the value of the <see cref="RepetitionDots"/> property.
        /// </summary>
        private ObservableCollection<Point> repetitionDots;
        /// <summary>
        /// Holds the value of the <see cref="Staves"/> collection.
        /// </summary>
        private ObservableCollection<Staff> staves;
        /// <summary>
        /// Holds the value of the <see cref="StaffConnectors"/> property.
        /// </summary>
        private ObservableCollection<StaffConnector> staffConnectors;
        
        #endregion
    }
}
